#!/ust/bin/env python3
# encoding: utf-8
"""
last edited at 2019-10-29
by Stone at BlueNet

封装通信协议。提供通信接口，将调用者传入的数据按协议封装成帧，通过串口发送出去，或者解析串口传来的数据。
通信协议基于ASCII进行定义，详见doc.
"""
import logging
import serial

ENCODING = 'utf-8'  # 默认使用的编码
BOL = b'<'          # Begin of Line
EOL = b'>'          # End of Line
SEP_CMD = b':'      # Separator cmd
SEP_VAL = b','      # Separator Value


class Communication(serial.Serial):
    """
    1. 为保证通信可靠，将处理串口的代码单独封装，以便于处理一些异常。
    emmmm... 但是目前不知道可能出现哪些异常，有了也不知道该怎么处理，所以就先不处理了orz。
    2. 封帧等通信协议在该类中完成，该类负责解决可靠传输负载的问题。

    由于主要还是直接使用串口的read(), write(), readline(), writeline()，所以采用了继承的方案。

    通信协议: refer to readme.md

    需要注意的几点:

        建议使用with语句打开串口(以及本对象):
            with serial.Serial('com1') as ser:
                your_loop()

        为了避免阻塞, 设置了timeout(默认), 读取时可能接收到不完整的信息. 一个可能的例子就是readline(),
        假如没有设置超时, 当下位机发送的 '/n' 丢失时, 函数会一直等待, 直到收到 '/n'. 而如果设置了超时,
        则可能下位机发来的数据还未收完, 函数就已退出返回, 导致数据不完整. 总的来说, 单线程模型下, 设置
        一个超时利大于弊, 也足够好用了. 日后如有需求, 可以考虑多线程或者ansyio以避免阻塞.

        pyserial文档: https://pyserial.readthedocs.io/en/latest/

        pyserial收发的都是bytes()类型, python的字符串需要先转换成bytes()才能发送, 转换方法为
        str.encoding('utf-8'). 为了方便, 这个转换过程类中的write_frame, read_frame 函数中自动完成,
        但是需要注意传入的字符串要符合对应的编码. 默认编码从__init__中传入, 可在config.py中设置.

        读取串口可用的方法(详见pyserial文档):
            read(size=1), read_until(expected=LF, size=None), readline(size=-1), readlines(hint=-1)
            如果没有收到任何数据, 返回b''

        发送串口可用的方法:
            write(data), writelines(lines)

        其他一些可能有用的方法:
            open(), close(), reset_input_buffer(), reset_output_buffer()

        一些可能有用的属性:
            out_waiting: Get the number of bytes in the output buffer.
            in_waiting: Get the number of bytes in the input buffer.
    """
    def __init__(self, port, baudrate=115200, timeout=0.01, encoding=ENCODING, *args, **kwargs):
        super().__init__(port=port, baudrate=baudrate, timeout=timeout, *args, **kwargs)
        self._encoding = encoding

    def read_frame(self):
        """
        Read a frame of message, parse it and then return the cmd and values. If nothing received,
        it returns an empty string and an empty list.

        The format of a frame is
            "BOL cmd=val1, val2, val3, ... EOL"

        for details please refer to readme.md

        :return: <str> cmd, <tuple> values
        """
        frame = bytearray()

        recv = self.read_until(BOL)
        frame.extend(recv)

        # recv[-1] -> int; recv[-1:] -> bytes, source of bug orz
        if recv and recv[-1:] == BOL:
            recv = self.read_until(EOL)
            frame.extend(recv)

            if recv and recv[-1:] == EOL:
                recv = recv[:-1]
                try:
                    # 有等号则分离cmd和values, 无等号则赋值values空字符串
                    cmd, values = recv.split(SEP_CMD) if SEP_CMD in recv else (recv, b'')

                    cmd = cmd.decode(self._encoding)
                    values = values.split(SEP_VAL)
                    values = [float(x) for x in values if x]  # 应对 "1,2,3,".split(',') -> ["1", "2", "3", ""] 的情况
                    values = [int(x) if x.is_integer() else x for x in values]  # 如果小数部分为0则转换为整数,否则仍未小数

                except ValueError:  # too many values to unpack
                    # 已检查帧首, 帧尾, 此时若出错应当为负载的格式有问题
                    logging.warning('Try to parse "{}" failed'.format(frame))
                    return '', []
            else:
                return '', []
        else:
            return '', []

        return cmd, values

    def write_frame(self, cmd, values):
        """
        将 cmd 和 values 组装成帧, 转换成bytes, 然后通过串口发送出去.

        The format of a frame is
            "BOL cmd=val1, val2, val3, ... EOL"

        for details please refer to readme.md

        :param cmd: <str>
        :param values: <list> 列表中的元素只能是整数或小数
        :return:
        """
        for x in values:
            if not isinstance(x, (int, float)):
                raise ValueError('Only int and float are supported. But {} {} in {} is neither of them'
                                 .format(type(x), repr(x), values))

        # 先转换为bytes, 然后封装成帧发送出去.
        values = [bytes(str(x), encoding=self._encoding) for x in values]

        values = SEP_VAL.join(values)
        cmd = cmd.encode(self._encoding)

        frame = b'%s%s%s%s%s' % (BOL, cmd, SEP_CMD, values, EOL)  # pingpong protocol v1.0
        # frame = b'%s%s%s%s' % (BOL, cmd, values, EOL)  # pingpong protocol v1.1

        self.write(frame)


logger = logging.getLogger(__name__)


if __name__ == '__main__':
    logging_config = {
        'level': logging.DEBUG,
        'format': '%(asctime)s %(levelname)s - %(name)s || %(message)s',
        'datefmt': '%m%d %H:%M:%S'
    }
    # logging_config.update({'filename': time.strftime(cfg.LOG_FILENAME_FORMAT)})
    logging.basicConfig(**logging_config)

    with Communication('com3') as comm:
        comm.write_frame('X', [50.50, 60.66])
        for i in range(100):
            comm.write_frame('X', [50.50, 60.66])
            print(comm.read_frame())
